﻿//-----------------------------------------------------------------------
// <copyright file="RegistryScanner.cs" company="FastNET Services">
//     Copyright (c) 2007-2009. All Rights Reserved.
// </copyright>
// <license>
//     GPLv2
// </license>
// <warranty>
//     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
//     BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//     DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </warranty>
//-----------------------------------------------------------------------
#region Usings

using System;
using System.Collections.Generic;
using System.IO;
using System.Security;
using System.Text.RegularExpressions;
using FastNET.CleanAct.Client.Interfaces.Jobs;
using FastNET.CleanAct.Client.Interfaces.TaskProcessing.Scanners;
using FastNET.CleanAct.Client.Interfaces.Tasks;
using FastNET.CleanAct.Client.Win32;
using FastNET.CleanAct.Common.Configuration.Actions.RegistryActions;
using FastNET.CleanAct.Common.Core;
using Microsoft.Win32;
using FastNET.CleanAct.Client.CleanEngineBusiness;
using FastNET.CleanAct.Client.TaskProcessing.Basic.Tasks.Registry;

#endregion

namespace FastNET.CleanAct.Client.TaskProcessing.Basic.Scanners
{
    /// <summary>
    /// Class for managing all file system related scanning activities
    /// </summary>
    public class RegistryScanner : TaskProcessor, IScanner
    {
        private const string PREFIX_REGLOAD = "CleanAct-";

        private static readonly object _regKeyLock;
        private static readonly Dictionary<string, string> _loadedUserKeys;

        #region CTor/DTor

        static RegistryScanner()
        {
            _regKeyLock = new object();
            _loadedUserKeys = new Dictionary<string, string>();
        }

        #endregion

        #region Public Processor Info Properties

        /// <summary>
        /// Gets the name of the processor for reporting
        /// </summary>
        public override string ProcessorName
        {
            get
            {
                return "Registry Scanner";
            }
        }

        /// <summary>
        /// Gets a description of the processor for reporting
        /// </summary>
        public override string ProcessorDescription
        {
            get
            {
                return "Scanning processor for all registry related activity";
            }
        }

        #endregion

        #region Public Processing Methods

        /// <summary>
        /// Performs a scan based on the requirements of the scanner and returns a list of tasks that will be processed
        /// </summary>
        /// <param name="action">IJobAction to process for task generation</param>
        /// <param name="actionID">ID to associate the task to the action</param>
        /// <returns>List of ITask for all the tasks that need to be performed</returns>
        public IList<ITask> PerformScan(IJobAction action, Guid actionID)
        {
            List<ITask> tasks = new List<ITask>();

            if (action.ActionType.ToLower() != "registry" || string.IsNullOrEmpty(action.Configuration))
                return tasks;

            RegistryActionSettings settings = RegistryActionSettings.LoadXml(action.Configuration);

            //TODO add logging

            if (string.IsNullOrEmpty(settings.ValueType))
                return tasks;

            if (string.IsNullOrEmpty(settings.Value))
                return tasks;

            if (string.IsNullOrEmpty(settings.Path))
                return tasks;

            //Validate Pattern
            try
            {
                Regex.Match("", settings.Pattern);
            }
            catch (ArgumentException)
            {
                return tasks;
            }
    
            //Pick the root key based on the type of scan
            switch (settings.Scope)
            {
                case ActionScope.Computer:
                    //Registry.LocalMachine;
                    tasks.AddRange(ScanBaseKey(action.RecursiveMode, Registry.LocalMachine, settings));
                    break;

                case ActionScope.User:
                    List<DynamicSetting> profilePaths = DynamicSetting.RetrieveByKeyName(KnownUserFolderType.Profile.ToString());

                    //S-1-5-18 = Local System Account
                    RegistryKey profileKey = Registry.Users.OpenSubKey("S-1-5-18");
                    tasks.AddRange(ScanBaseKey(action.RecursiveMode, profileKey, settings));

                    //S-1-5-19 = Local Service
                    profileKey = Registry.Users.OpenSubKey("S-1-5-19");
                    tasks.AddRange(ScanBaseKey(action.RecursiveMode, profileKey, settings));

                    //S-1-5-20 = Network Service
                    profileKey = Registry.Users.OpenSubKey("S-1-5-20");
                    tasks.AddRange(ScanBaseKey(action.RecursiveMode, profileKey, settings));

                    foreach (DynamicSetting setting in profilePaths)
                    {
                        profileKey = AquireUserKeyReference(!setting.ProfileID.HasValue ? "DefaultUser" : setting.Profile.AccountName, setting.Value);
                        tasks.AddRange(ScanBaseKey(action.RecursiveMode, profileKey, settings));
                    }

                    break;
            }

            return tasks;
        }

        #endregion

        #region Private Static Scanning Methods

        /// <summary>
        /// Method to begin the scanning process given a root key, recursive mode, and settings
        /// </summary>
        /// <param name="recursiveMode">ActionRecursiveMode with the recursive mode to process by</param>
        /// <param name="rootKey">RegistryKey with the root registry key to use during processing</param>
        /// <param name="settings">RegistryActionSettings with the settings for the scan</param>
        /// <returns>List of ITask for everything that is to be processed</returns>
        private static IList<ITask> ScanBaseKey(ActionRecursiveMode recursiveMode, RegistryKey rootKey, RegistryActionSettings settings)
        {
            List<ITask> tasks = new List<ITask>();

            RegistryKey baseKey;

            //Set Base Path Here
            try
            {
                baseKey = rootKey.OpenSubKey(settings.Path, false);
            }
            catch (Exception)
            {
                baseKey = null;
            }
            
            if (baseKey == null)
                return tasks;

            //If the recursivemode is none, there should be nothing processed
            if (recursiveMode == ActionRecursiveMode.None)
            {
                return tasks;
            }

            //If the recursivemode is SubItems, start by processing SubKeys
            if (recursiveMode == ActionRecursiveMode.SubItems || recursiveMode == ActionRecursiveMode.ThisAndSubItems)
            {
                tasks.AddRange(ScanSubKeys(baseKey, settings));
            }

            //If the recursivemode is ThisItem, add the values next
            if (recursiveMode == ActionRecursiveMode.ThisItem || recursiveMode == ActionRecursiveMode.ThisAndSubItems)
            {
                tasks.AddRange(ScanThisKey(rootKey, settings));
            }

            return tasks;
        }

        /// <summary>
        /// Recursive method to scan through all the sub keys and find matches
        /// </summary>
        /// <param name="baseKey">Base RegistryKey to scan for items</param>
        /// <param name="settings">RegistryActionSettings with all the settings for the scan</param>
        /// <returns>List of ITask for everything that is to be processed</returns>
        private static IList<ITask> ScanSubKeys(RegistryKey baseKey, RegistryActionSettings settings)
        {
            List<ITask> tasks = new List<ITask>();

            //Go through all the sub keys first
            foreach (string value in baseKey.GetSubKeyNames())
            {
                RegistryKey subkey;

                //Try to open the subkey and continue if there is a problem
                try
                {
                    subkey = baseKey.OpenSubKey(value, false);
                }
                catch (ObjectDisposedException)
                    //The RegistryKey is closed (closed keys cannot be accessed). 
                {
                    //TODO, log this when logging is implemented
                    continue;
                }
                catch (SecurityException)
                    //The user does not have the permissions required to access the registry key in the specified mode. 
                {
                    //TODO, log this when logging is implemented
                    continue;
                }

                tasks.AddRange(ScanSubKeys(subkey, settings));
            }

            tasks.AddRange(ScanThisKey(baseKey, settings));

            return tasks;
        }

        /// <summary>
        /// Method to scan a specific key for anything to maintain
        /// </summary>
        /// <param name="baseKey">Base RegistryKey to scan for items</param>
        /// <param name="settings">RegistryActionSettings with all the settings for the scan</param>
        /// <returns>List of ITask for everything that is to be processed</returns>
        private static IList<ITask> ScanThisKey(RegistryKey baseKey, RegistryActionSettings settings)
        {
            List<ITask> tasks = new List<ITask>();
            Regex tester;

            //Try to create a regex for multiple uses. This will fail if the pattern is invalid
            try
            {
                tester = new Regex(settings.Pattern);
            }
            catch (ArgumentException)
            {
                //TODO, log this when logging is implemented
                return tasks;
            }

            //If the target for checking includes Values, Scan the sub values
            if (settings.Target == ActionTarget.Value || settings.Target == ActionTarget.All)
            {
                List<string> values = new List<string>();

                foreach (string value in baseKey.GetValueNames())
                {
                    if (!tester.IsMatch(value))
                    {
                        continue;
                    }

                    values.Add(value);
                }

                tasks.Add(CreateTask(settings.Action,settings.Value, settings.ValueType, baseKey, values, false));
            }

            //If the target for checking includes Keys, Scan the sub keys
            if (settings.Target == ActionTarget.Key || settings.Target == ActionTarget.All)
            {
                List<string> values = new List<string>();

                foreach (string value in baseKey.GetSubKeyNames())
                {
                    if (!tester.IsMatch(value))
                    {
                        continue;
                    }

                    values.Add(value);
                }

                tasks.Add(CreateTask(settings.Action, settings.Value, settings.ValueType, baseKey, values, true));
            }

            return tasks;
        }

        #endregion

        #region Task Management

        /// <summary>
        /// Builds a task for cleanup based on the input provided and returns a generic ITask for processing
        /// </summary>
        /// <param name="action">ActionType for the process</param>
        /// <param name="itemValue">Object with the value that is to be set</param>
        /// <param name="itemValueType">RegistryValueKind string encoding to set as the value type</param>
        /// <param name="baseKey">Base processing Key</param>
        /// <param name="values">Values to process</param>
        /// <param name="keyItems">Boolean whether or not the values refer to keys or values</param>
        /// <returns></returns>
        private static ITask CreateTask(ActionType action, Object itemValue, string itemValueType, RegistryKey baseKey, IEnumerable<string> values, bool keyItems)
        {
            RegistryTask task;

            switch (action)
            {
                case ActionType.Create:
                    task = new CreateRegistryTask
                                  {
                                      Value = itemValue,
                                      ValueType =
                                          ((RegistryValueKind)
                                           Enum.Parse(typeof(RegistryValueKind), itemValueType))
                                  };
                    break;
                case ActionType.Delete:
                    task = new DeleteRegistryTask();
                    break;
                case ActionType.Modify:
                    task = new ModifyRegistryTask
                    {
                        Value = itemValue
                    };
                    break;
                default:
                    return null;
            }

            //Add the ID/Value for all entries to work with
            foreach (string value in values)
            {
                task.SourceEntries.Add(task.SourceEntries.Count, value);
            }

            task.ProcessKeys = keyItems;

            string registryKey = baseKey.ToString();

            //Breaks up the registry key to its parts to rebuild later
            if (registryKey.StartsWith("HKEY_LOCAL_MACHINE", StringComparison.InvariantCultureIgnoreCase))
            {
                task.RegistryPath = registryKey.Remove(0, 19); //Remove HKEY_LOCAL_MACHINE/
            }
            else if (registryKey.StartsWith("HKEY_USERS", StringComparison.InvariantCultureIgnoreCase))
            {
                string[] parts = registryKey.Split(new[] {'/'}, 3, StringSplitOptions.RemoveEmptyEntries);

                task.UserID = parts[1].StartsWith(PREFIX_REGLOAD, StringComparison.InvariantCultureIgnoreCase)
                    ? parts[1].Remove(0, PREFIX_REGLOAD.Length + 1) : parts[1]; //Remove CleanAct- or return the user sid

                task.RegistryPath = parts[2];
            }

            return task;
        }

        #endregion

        #region Registry Load/Unload

        /// <summary>
        /// Locates and returns a base path to a user's hive or loads it if neccesary
        /// </summary>
        /// <param name="SID">SID of the user to locate or load</param>
        /// <param name="profilePath">Path to the profile on disk of the user</param>
        /// <returns>RegistryKey pointing to a user's hive</returns>
        private static RegistryKey AquireUserKeyReference(string SID, string profilePath)
        {
            //Try to find a loaded user entry
            foreach (String entry in Registry.Users.GetSubKeyNames())
            {
                if (entry.ToLower() != SID)
                    continue;
                
                try
                {
                    return Registry.Users.OpenSubKey(SID, false);
                }
                catch (Exception)
                {
                    return null;
                }
            }

            //Lock so anything requesting can get the latest list
            lock (_regKeyLock)
            {

                //Since the logged in user entry is not there, we will want to check our cache
                if (_loadedUserKeys.ContainsKey(SID))
                {
                    try
                    {
                        return Registry.Users.OpenSubKey(String.Format("{0}{1}", PREFIX_REGLOAD, SID), false);
                    }
                    catch (Exception)
                    {
                        return null;
                    }
                }

                //If the profile is not already in the registry or found in our cache we need to load it
                string tempPath = CreateTempRegistryCopy(profilePath);

                //If the temp file is empty then exit
                if (string.IsNullOrEmpty(tempPath))
                {
                    return null;
                }

                if (!WindowsRegistry.LoadRegistry(tempPath, String.Format(String.Format("{0}{1}", PREFIX_REGLOAD, SID))))
                {
                    return null;
                }

                _loadedUserKeys.Add(SID, tempPath);

                try
                {
                    return Registry.Users.OpenSubKey(String.Format("{0}{1}", PREFIX_REGLOAD, SID), false);
                }
                catch (Exception)
                {
                    return null;
                }
            }
        }

        /// <summary>
        /// Creates a temporary file of the user's registry in case the user decides to log on.
        /// </summary>
        /// <param name="profilePath">Path to the user's profile</param>
        /// <returns>String containing the full path and filename of the temporary registry file</returns>
        private static string CreateTempRegistryCopy(string profilePath)
        {
            string tempPath = Path.Combine(profilePath, "CleanAct.NTUSER.DAT");
            string sourcePath = string.Empty;

            if (File.Exists(Path.Combine(profilePath, "NTUSER.DAT")))
            {
                sourcePath = Path.Combine(profilePath, "NTUSER.DAT");
            }
            else if (File.Exists(Path.Combine(profilePath, "NTUSER.MAN")))
            {
                sourcePath = Path.Combine(profilePath, "NTUSER.MAN");
            }

            if (string.IsNullOrEmpty(sourcePath))
            {
                return null;
            }

            File.Copy(sourcePath, tempPath, true);

            return tempPath;
        }

        /// <summary>
        /// Cleans up all temporary loaded registry hives
        /// </summary>
        public static void ReleaseResources()
        {
            lock (_regKeyLock)
            {
                foreach (KeyValuePair<string, string> userKey in _loadedUserKeys)
                {
                    WindowsRegistry.UnloadUserRegistry(String.Format("{0}{1}", PREFIX_REGLOAD, userKey.Key));

                    if (File.Exists(userKey.Value))
                    {
                        File.Delete(userKey.Value);
                    }
                }
                _loadedUserKeys.Clear();
            }
        }

        #endregion
    }
}